#define _GNU_SOURCE
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <dirent.h>
#include <limits.h>
#include "log.h"
#include "radix.h"
#include "partialfs.h"

#define CHECK_PATH(path, path_len) \
    if (!(path_len)) (path_len) = strlen((path)); \
    if (!(path_len)) { \
        pfs_log_err(("empty path")); \
        return -1; \
    } \
    if ((path)[0] != '/') { \
        pfs_log_err(("'%.*s' not starts with '/'", path_len, path)); \
        return -1; \
    }

#define IS_FPATH(path, path_len) ((path)[(path_len) - 1] != '/')
#define IS_DPATH(path, path_len) ((path)[(path_len) - 1] == '/')

#define FH_STORE_INT(fh, _i) ((fh_t *)(&(fh)))->i= (int)(_i)
#define FH_STORE_PTR(fh, _ptr) ((fh_t *)(&(fh)))->ptr = (void *)(_ptr)
#define FH_AS_INT(fh) (((fh_t *)(&(fh)))->i)
#define FH_AS_PTR(fh) (((fh_t *)(&(fh)))->ptr)

/* helper structure to manuplate fh field in file info
 */
typedef union {
    int i;
    void * ptr;
    uint64_t u64;
} fh_t;

/* path control 
 */
typedef struct path_ctrl_t {
    /* these attributes are used both by file path and dir path
     */
    // is it a file path (not ends with '/')
    int is_fpath;                   

    // for '/' and '/usr' path_level is 1
    // for '/usr/' and '/usr/local' path_level is 2
    int path_level;                     

    // is it allowed
    int allow;

} path_ctrl_t;

static rdx_tree_t hier_ctrl;

static int is_pfs_init = 0;

void pfs_init() {
    int r;

    if (is_pfs_init)
        return;
    rdx_tree_init(&hier_ctrl);
    r = pfs_deny_path("/", 1);
    assert(r == 0);
    is_pfs_init = 1;
    pfs_log_info(("file system init done"));
}

static int _pfs_ctrl_path(const char * path, size_t path_len, int allow) {

    rdx_node_t * node;
    path_ctrl_t * pctl;
    const char * p;
    int err;

    CHECK_PATH(path, path_len);

    // insert
    node = rdx_tree_ensure(&hier_ctrl, path, path_len, &err);
    if (err) {
        pfs_log_err(("not enough memory (radix tree)"));
        return -1;      // not enough memory
    }

    pctl = (path_ctrl_t *)node->val;
    if (!pctl) {
        pctl = (path_ctrl_t *)malloc(sizeof(path_ctrl_t));
        if (!pctl) {
            pfs_log_err(("not enough memory (path control)"));
            return -1;
        }
    }

    // fill
    pctl->is_fpath = IS_FPATH(path, path_len);
    pctl->path_level = 0;
    p = path;
    while (p < path + path_len) {
        if (*p++ == '/')
            ++pctl->path_level;
    }
    pctl->allow = allow ? 1 : 0;
    node->val = (void *)pctl;
    pfs_log_info(("%s '%.*s' (%s, %d)", allow ? "allow" : "deny",
            path_len, path, pctl->is_fpath ? "file path" : "dir path",
            pctl->path_level));
    return 0;
}

int pfs_allow_path(const char * path, size_t path_len) {
    return _pfs_ctrl_path(path, path_len, 1);
}

int pfs_deny_path(const char * path, size_t path_len) {
    return _pfs_ctrl_path(path, path_len, 0);
}

// iternal used only
static inline int _prefix_is_path_prefix(rdx_node_t * pfx, 
        const char * path) {

    path_ctrl_t * pctl;
    char e;

    pctl = (path_ctrl_t *)pfx->val;
    if (!pctl->is_fpath)
        return 1;
    // '/usr' is a prefix of '/usr1/local' 
    // but not a path prefix of it
    e = path[pfx->keylen];
    if (e == '/' || e == '\0')
        return 1;
    return 0;
}

static rdx_node_t * _path_prefix_iter_begin(const char * path, 
        size_t path_len,
        rdx_prefix_iter_t * iter) {

    rdx_node_t * pfx;

    pfx = rdx_prefix_iter_begin(&hier_ctrl, path, path_len, iter);
    while (pfx) {
        if (_prefix_is_path_prefix(pfx, path)) {
            pfs_log_verbose((" %c%s", 
                    ((path_ctrl_t *)pfx->val)->allow ? '+' : '-', pfx->key));
            return pfx;
        }
        pfx = rdx_prefix_iter_next(iter);
    }
    return NULL;
}

static rdx_node_t * _path_prefix_iter_next(rdx_prefix_iter_t * iter) {

    rdx_node_t * pfx;
    const char * path;

    path = iter->key;
    while (1) {
        pfx = rdx_prefix_iter_next(iter);
        if (!pfx)
            break;
        if (_prefix_is_path_prefix(pfx, path)) {
            pfs_log_verbose((" %c%s", 
                    ((path_ctrl_t *)pfx->val)->allow ? '+' : '-', pfx->key));
            return pfx;
        }
    }
    return NULL;
}

int pfs_get_path_visibility(const char * path, 
        size_t path_len) {

    rdx_prefix_iter_t iter;
    rdx_node_t * pfx;
    path_ctrl_t * pctl;
    int default_allow, expect_level;
    int remain;

    CHECK_PATH(path, path_len);

    // convert a dir path to file path
    if (IS_DPATH(path, path_len)) {
        --path_len;
        // root always visible
        if (!path_len)
            return 1;
    }

    pfs_log_verbose(("get path visibility: '%.*s'", path_len, path));

    // '/' should always here
    pfx = _path_prefix_iter_begin(path, path_len, &iter);
    assert(pfx && pfx->key[0] == '/' && pfx->keylen == 1);
    pctl = (path_ctrl_t *)pfx->val;

    remain = path_len - pfx->keylen;
    default_allow = pctl->allow;
    expect_level = pctl->path_level;
    assert(expect_level == 1);

    while (1) {
        pfx = _path_prefix_iter_next(&iter);
        if (!pfx)
            break;
        pctl = (path_ctrl_t *)pfx->val;
        remain = path_len - pfx->keylen;

        // we are under an invisible dir
        // and at least one intermediate path is not in 
        // hier_ctrl so by default its invisible
        if (!default_allow && pctl->path_level != expect_level) {
            pfs_log_verbose(("path invisible since in invisible dir"));
            return 0;
        }

        // file path
        if (pctl->is_fpath) {
            // explicit deny
            if (!pctl->allow) {
                pfs_log_verbose(("path invisible since explicit declare"));
                return 0;
            }
            ++expect_level;
            continue;
        }

        // dir path
        default_allow = pctl->allow;
        expect_level = pctl->path_level;
    }

    // in a visible dir
    // or no remain
    if (default_allow|| !remain) {
        pfs_log_verbose(("path visible"));
        return 1;
    }

    pfs_log_verbose(("path invisible"));
    return 0;

}

static inline int pfs_err(const char * fname) {
    pfs_log_debug((" %s err: %s", fname, strerror(errno)));
    return -errno;
}

/* fuse ops 
 */


int partialfs_getattr(const char * path, struct stat * stbuf) {
    int r;

    pfs_log_debug(("==> getattr('%s', %p)", path, stbuf));

    if (!pfs_get_path_visibility(path, 0)) {
        pfs_log_debug((" getattr err: %s", strerror(ENOENT)));
        return -ENOENT;
    }

    r = lstat(path, stbuf);
    if (r < 0)
        r = pfs_err("getattr");

    return r;
}

int partialfs_readlink(const char * path, char * buf, size_t sz) {
    int r;

    pfs_log_debug(("==> readlink('%s', %p, %d)", path, buf, sz));

    if (!pfs_get_path_visibility(path, 0)) {
        pfs_log_debug((" readlink err: %s", strerror(ENOENT)));
        return -ENOENT;
    }

    r = readlink(path, buf, sz - 1);
    if (r < 0)
        r = pfs_err("readlink");
    else {
        buf[r] = '\0';
        r = 0;
    }

    return r;
}

int partialfs_mknod(const char * path, mode_t mode, dev_t dev) {
    return -EROFS;
}

int partialfs_mkdir(const char * path, mode_t mode) {
    return -EROFS;
}

int partialfs_unlink(const char * path) {
    return -EROFS;
}

int partialfs_rmdir(const char * path) {
    return -EROFS;
}

int partialfs_symlink(const char * path, const char * link) {
    return -EROFS;
}

int partialfs_rename(const char * path, const char * newpath) {
    return -EROFS;
}

int partialfs_link(const char * path, const char * link) {
    return -EROFS;
}

int partialfs_chmod(const char * path, mode_t mode) {
    return -EROFS;
}

int partialfs_chown(const char * path, uid_t uid, gid_t gid) {
    return -EROFS;
}

int partialfs_truncate(const char * path, off_t off) {
    return -EROFS;
}

int partialfs_open(const char * path, struct fuse_file_info * fi) {

#if 0
    static int allow_mask = ~(O_RDONLY | O_EXCL 
        | O_NOCTTY
        | O_NONBLOCK
        | O_NDELAY
        | O_ASYNC
#ifdef __USE_GNU
        | O_DIRECTORY
        | O_NOFOLLOW
        | O_NOATIME
        | O_CLOEXEC
#endif
        );
#endif
    int r, fd;

    pfs_log_debug(("==> open('%s', flags=%d, direct_io=%u, keep_cache=%u, "
                "flush=%u)", path, fi->flags, fi->direct_io,
                fi->keep_cache, fi->flush));

#if 0
    if (fi->flags & allow_mask) {
        pfs_log_debug((" %s <== open", strerror(EROFS)));
        return -EROFS;
    }
#endif

    if (!pfs_get_path_visibility(path, 0)) {
        pfs_log_debug((" open err: %s", strerror(ENOENT)));
        return -ENOENT;
    }

    fd = open(path, fi->flags);
    if (fd < 0)
        r = pfs_err("open");
    else {
        FH_STORE_INT(fi->fh, fd);
        r = 0;
        pfs_log_debug((" %d <== open", fd));
    }
    return r;
}

int partialfs_read(const char * path, char * buf, size_t size, off_t off,
         struct fuse_file_info * fi) {
    int r;

    pfs_log_debug(("==> read('%s', fd=%d, size=%d, off=%d)", path, 
                FH_AS_INT(fi->fh), size, off));

    r = pread(FH_AS_INT(fi->fh), buf, size, off);
    if (r < 0)
        r = pfs_err("read");
    return r;
}

int partialfs_write(const char * path, const char * buf, size_t size, off_t off,
          struct fuse_file_info * fi) {
    return -EROFS;
}

int partialfs_statfs(const char * path, struct statvfs * stat) {
    return -ENOSYS;
}

int partialfs_flush(const char * path, struct fuse_file_info * fi) {
    return 0;
}

int partialfs_release(const char * path, struct fuse_file_info * fi) {
    return close(FH_AS_INT(fi->fh));
}

int partialfs_fsync(const char * path, int datasync, struct fuse_file_info * fi) {
    return 0;
}

int partialfs_opendir(const char * path, struct fuse_file_info * fi) {
    DIR * dp;

    pfs_log_debug(("==> opendir('%s', %p)", path, fi));

    if (!pfs_get_path_visibility(path, 0)) {
        pfs_log_debug((" opendir err: %s", strerror(ENOENT)));
        return -ENOENT;
    }

    dp = opendir(path);
    if (dp == NULL)
        return pfs_err("opendir");
    FH_STORE_PTR(fi->fh, dp);
    
    return 0;
}

int partialfs_readdir(const char * path, void * buf, fuse_fill_dir_t filler, 
        off_t off,
        struct fuse_file_info * fi) {
    DIR * dp;
    struct dirent * de;
    const char * d_name;
    size_t path_len, sub_path_len;
    char sub_path[PATH_MAX + 1];

    pfs_log_debug(("==> readdir('%s', %p, %p, %d, %p)", path, buf,
                filler, off, fi));

    // make it a file path
    path_len = strlen(path);
    if (IS_DPATH(path, path_len))
        --path_len;

    dp = (DIR *)FH_AS_PTR(fi->fh);
    errno = 0;
    sub_path[PATH_MAX] = '\0';

    seekdir(dp, off);
    while ((de = readdir(dp)) != NULL) {
        d_name = de->d_name;

        // don't check '..' and '.'
        if (d_name[0] == '.' && (d_name[1] == '\0' || (d_name[1] == '.' 
                        && d_name[2] == '\0')))
            goto FILL_DIR_ENTRY;

        // make the full path of a dir entry
        sub_path_len = snprintf(sub_path, PATH_MAX, "%.*s/%s", 
                path_len, path, d_name);
        // this should not happen since the origin file system
        // will guarantee that no path can exceed PATH_MAX
        assert(sub_path_len < PATH_MAX);

        // check visibility
        if (!pfs_get_path_visibility(sub_path, sub_path_len))
            continue;
        
FILL_DIR_ENTRY:
        // get the offset of the next entry
        off = telldir(dp);
        if (filler(buf, d_name, NULL, off))
            break;
    }

    // err
    if (errno > 0)
        return pfs_err("readdir");

    return 0;

}

int partialfs_releasedir(const char * path, struct fuse_file_info * fi) {
    int r;

    pfs_log_debug(("==> releasedir('%s', %p)", path, fi));

    r = closedir((DIR *)FH_AS_PTR(fi->fh));
    if (r < 0)
        return pfs_err("releasedir");
    return r;
}

int partialfs_fsyncdir(const char * path, int datasync, struct fuse_file_info * fi) {
    return 0;
}

void * partialfs_init(struct fuse_conn_info * conn) {
    if (!is_pfs_init)
        pfs_init();
    return NULL;
}

void partialfs_destroy(void * userdata) {
    rdx_iter_t iter;
    rdx_node_t * node;

    node = rdx_iter_begin(&hier_ctrl, &iter);
    while (node) {
        free(node->val);
        node = rdx_iter_next(&iter);
    }
    
    rdx_tree_fini(&hier_ctrl);

    pfs_log_info(("file system fini done"));
}

int partialfs_access(const char * path, int mask) {
    int r;

    pfs_log_debug(("==> access('%s', %d)", path, mask));

    if (!pfs_get_path_visibility(path, 0)) {
        pfs_log_debug((" access err: %s", strerror(ENOENT)));
        return -ENOENT;
    }

    r = access(path, mask);
    if (r < 0)
        return pfs_err("access");
    return r;
}

int partialfs_create(const char * path, mode_t mode, struct fuse_file_info * fi) {
    return -EROFS;
}

int partialfs_ftruncate(const char * path, off_t off, struct fuse_file_info * fi) {
    return -EROFS;
}

int partialfs_fgetattr(const char * path, struct stat * statbuf, struct fuse_file_info * fi) {
    int r;

    pfs_log_debug(("==> fgetattr('%s', %p, %p)", path, statbuf, fi));

    r = fstat(FH_AS_INT(fi->fh), statbuf);
    if (r < 0)
        return pfs_err("fgetattr");
    return r;
}

struct fuse_operations partialfs_oper = {
    .getattr        = partialfs_getattr,
    .readlink       = partialfs_readlink,
    .mknod          = partialfs_mknod,
    .mkdir          = partialfs_mkdir,
    .unlink         = partialfs_unlink,
    .rmdir          = partialfs_rmdir,
    .symlink        = partialfs_symlink,
    .rename         = partialfs_rename,
    .link           = partialfs_link,
    .chmod          = partialfs_chmod,
    .chown          = partialfs_chown,
    .truncate       = partialfs_truncate,
    .open           = partialfs_open,
    .read           = partialfs_read,
    .write          = partialfs_write,
    .statfs         = partialfs_statfs,
    .flush          = partialfs_flush,
    .release        = partialfs_release,
    .fsync          = partialfs_fsync,
#if 0
    .setxattr       = partialfs_setxattr,
    .getxattr       = partialfs_getxattr,
    .listxattr      = partialfs_listxattr,
    .removexattr    = partialfs_removexattr,
#endif
    .opendir        = partialfs_opendir,
    .readdir        = partialfs_readdir,
    .releasedir     = partialfs_releasedir,
    .fsyncdir       = partialfs_fsyncdir,
    .init           = partialfs_init,
    .destroy        = partialfs_destroy,
    .access         = partialfs_access,
    .create         = partialfs_create,
    .ftruncate      = partialfs_ftruncate,
    .fgetattr       = partialfs_fgetattr
};


